package org.dei.perla.sys.device.adapter;

import java.util.ArrayList;

import org.dei.perla.sys.device.channel.ChannelManager;
import org.dei.perla.sys.device.channel.frame.address.FrameAddress;
import org.dei.perla.utils.logger.LogRecord.Type;
import org.dei.perla.utils.logger.LogRecord.Verbosity;
import org.dei.perla.utils.messages.AdapterChannelMessage;
import org.dei.perla.utils.messages.Message;
import org.dei.perla.utils.messages.MsgUtility;
import org.dei.perla.utils.pipe.Pipe;

/**
 * Classe che gestisce un componente gateway
 */
public class Gateway extends AdapterServer{
	
	/**
	 * Rotte statiche
	 */
	private int [] pStaticRoutesSrcChannels;
	private int [] pStaticRoutesDstChannels;
	
	/**
	 * Liste dei canali lato server e lato client
	 */
	private ArrayList<ChannelManager> pServerChannel;
	private ArrayList<ChannelManager> pClientChannel;
	
	/**
	 * Lista degli indirizzi degli adapter lato server
	 */
	private ArrayList<FrameAddress> pAdapterServerAddress;
	
    /**
     * Costruttore
     * @param parChannels canali
     */
	public Gateway(ArrayList<ChannelManager> parChannels) {
		super("Gateway", parChannels);
	}
	
	/**
	 * Inizializza le rotte statiche, obbligatorio per far funzionare 
	 * il componente
	 * 
	 * @param parStaticRoutesSrcChannels id dei canali lato server
	 * @param parStaticRoutesDstChannels id dei canali lato client
	 * @param dstAddress indirizzi dei rispettivi adapter server
	 */
	public void setStaticRoutes(int [] parStaticRoutesSrcChannels,
			                     int [] parStaticRoutesDstChannels,
			                      FrameAddress[] dstAddress){
		
		this.pStaticRoutesSrcChannels = parStaticRoutesSrcChannels;
		this.pStaticRoutesDstChannels = parStaticRoutesDstChannels;
		
		this.pClientChannel = new ArrayList<ChannelManager>();
		this.pServerChannel = new ArrayList<ChannelManager>();
		this.pAdapterServerAddress = new ArrayList<FrameAddress>();
		
		for(int i = 0; i < this.pStaticRoutesDstChannels.length; i++){
			
			for(int j = 0; j < this.pChannelsMng.size(); j++){
				
				if(this.pChannelsMng.get(j).getChannelId() == this.pStaticRoutesDstChannels[i]){
					
					this.pServerChannel.add(this.pChannelsMng.get(j));
					this.pAdapterServerAddress.add(dstAddress[i]);
					break;
					
				}
				
			}
			
			for(int j = 0; j < this.pChannelsMng.size(); j++){
				
				if(this.pChannelsMng.get(j).getChannelId() == this.pStaticRoutesSrcChannels[i]){
					
					this.pClientChannel.add(this.pChannelsMng.get(j));
					break;
					
				}
				
			}
			
		}
	}

	
	@Override
	protected void initialize() {
		
		this.pChannelIdCounter = 1;
		
	}

	@Override
	protected void mngAckMessage(Message parMsg) {
		
		byte[] tPayload = parMsg.getPayload();
		int tBvci = MsgUtility.getBvci(tPayload);
		FrameAddress tAdapterAddress = parMsg.getAddress();
		int tVci = MsgUtility.getVciFromAckMessage(tPayload);
		
		// Memorizzo il nuovo Vci
		this.pReg.setVciFromBvciRemappedAndAdapterAddress(tVci, tBvci, tAdapterAddress);
		
		// Trovo l'indirizzo del device
		FrameAddress tDeviceAddress = this.pReg.getDeviceAddressFromVci(tVci);
		// Trovo il vecchio Bvci per mandare il mess di ack al client
		int tTrueBvci = this.pReg.getBvciFromVci(tVci);
		
		// Se lo trovo
		if(tTrueBvci != 0){
			
			byte[] tNewPayload = MsgUtility.createAckPayload(tTrueBvci, tVci);
			
			AdapterChannelMessage tMsg = new AdapterChannelMessage(tDeviceAddress, tNewPayload);
			
			// trovo la pipe di scrittura lato client
			Pipe<Message> tPipeOut = this.pReg.getLowerPipesFromVci(tVci)[1];
			
			if(tPipeOut != null){
				tPipeOut.enqueue(tMsg);
			}
			else{
				String tMessage = "Gateway: impossibile trovare pipe out per messaggio di ack positivo";
				loggerNotify(Type.Error, tMessage, Verbosity.Medium);
			}
			
		}else{
			String tMessage = "Gateway: impossibile trovare bvci non rimappato";
			loggerNotify(Type.Error, tMessage, Verbosity.Medium);
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	protected void mngBindingMsg(Message parMsg, Pipe<Message> parPipe) {
		
		// Ricavo informazioni dal messaggio
		byte[] tPayload = parMsg.getPayload();
		int tBvci = MsgUtility.getBvci(tPayload);
		FrameAddress tDeviceAddress = parMsg.getAddress();
		
		// Trovo l'indice nella lista relativo al canale in entrata
		int index = getIndexFromPipeIn(parPipe);
		
		if(index >= 0){
			
			Pipe tServerPipeIn = this.pServerChannel.get(index).getInputPipe();
			Pipe tServerPipeOut = this.pServerChannel.get(index).getOutputPipe();
			Pipe tClientPipeIn = this.pClientChannel.get(index).getInputPipe();
			Pipe tClientPipeOut = this.pClientChannel.get(index).getOutputPipe();
			FrameAddress tAdapterAddr = this.pAdapterServerAddress.get(index);
			
			Pipe<Message> currPipeOut = getPipeOutFromPipeIn(parPipe);
			
			// Aggiungo al registro
			boolean ok = this.pReg.addEntry(tBvci, tServerPipeIn, 
								tServerPipeOut, 
								 tClientPipeIn, 
								  tClientPipeOut, 
								   tDeviceAddress, tAdapterAddr);
			
			if(ok){
				
				// creo un bvci valido per creare un nuovo messaggio di binding
				int tRemappedBvci = this.pCreateUniqueChannelId();
				
				// Lo memorizzo nella routing table
				this.pReg.setRemappedBvciFromBvciAndDeviceAddress(tRemappedBvci, tBvci, tDeviceAddress);
				
				// Modifico il payload
				tPayload = MsgUtility.modifiedBvciBindingMessage(parMsg.getPayload(), tBvci, tRemappedBvci);
				
				// Creo un nuovo messaggio di binding con l'inidirizzo dell'adapter al quale spedirlo
				AdapterChannelMessage tMsg = new AdapterChannelMessage( tAdapterAddr, tPayload);
				// Server pipeout di default
				currPipeOut.enqueue(tMsg);
				
			}
			
			// errore nell'inserimento, oggetto già presente
			else{
				
				byte [] tNewPayload = MsgUtility.createNegativeAckPayload(tBvci);
				
				AdapterChannelMessage tMsg = new AdapterChannelMessage(tDeviceAddress, tNewPayload);
				
				Pipe tPipeOut = tClientPipeOut;
				
				if(tPipeOut != null){
					tPipeOut.enqueue(tMsg);
				}
				else{
					String tMessage = "Gateway: impossibile trovare pipe out per messaggio di negative binding";
					loggerNotify(Type.Error, tMessage, Verbosity.Medium);
				}
			}
		}
		else{
			
			String tMessage = "Gateway: impossibile trovare percorso statico per inoltrare messaggio";
			loggerNotify(Type.Error, tMessage, Verbosity.Low);		
			
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	protected void mngNegativeAckMsg(Message parMsg) {
		
		byte[] tPayload = parMsg.getPayload();
		int tBvci = MsgUtility.getBvci(tPayload);
		FrameAddress tAdapterAddress = parMsg.getAddress();
		
		// Trovo indirizzo del device
		FrameAddress tDeviceAddress = this.pReg.getDeviceAddressFromBvciRemappedAndAdapterAddress(tBvci,tAdapterAddress);
		// Trovo il Bvci valido
		int tTrueBvci = this.pReg.getBvciFromBvciRemappedAndAdapterAddress(tBvci, tAdapterAddress);
		
		if(tTrueBvci != 0){
			//Ricreo il messaggio di ack negativo
			byte[] tNewPayload = MsgUtility.createNegativeAckPayload(tTrueBvci);
			
			AdapterChannelMessage tMsg = new AdapterChannelMessage(tDeviceAddress, tNewPayload);
			
			Pipe tPipeOut = this.pReg.getLowerPipesFromBvciAndDeviceAddress(tTrueBvci, tDeviceAddress)[1];
			
			if(tPipeOut != null){
				tPipeOut.enqueue(tMsg);
				this.pReg.removeEntryByBvciAndDeviceAddress(tTrueBvci, tDeviceAddress);
			}
			else{
				String tMessage = "Gateway: impossibile trovare pipe out per messaggio di ack positivo";
				loggerNotify(Type.Error, tMessage, Verbosity.Medium);
			}
		}else {
			String tMessage = "Gateway: impossibile trovare bvci non rimappato";
			loggerNotify(Type.Error, tMessage, Verbosity.Medium);
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	protected void mngNormalMsg(Message parMsg, Pipe parPipe) {
		
		byte[] tPayload = parMsg.getPayload();
		int tVci = MsgUtility.getVciNormalMessage(tPayload);
		
		Pipe<Message>[] tPipesOut = this.pReg.getPipesOutFromVci(tVci);
		Pipe<Message>[] tPipesIn  = this.pReg.getPipesInFromVci(tVci);
		Pipe tPipeOut = null;
		FrameAddress tAddress = null;
		
		if(tPipesIn[1].equals(parPipe)){
			tAddress = this.pReg.getAdapterAddressFromVci(tVci);
			tPipeOut = tPipesOut[0];
		}
		else if(tPipesIn[0].equals(parPipe)){
			tAddress = this.pReg.getDeviceAddressFromVci(tVci);
			tPipeOut = tPipesOut[1];
		}
		
		AdapterChannelMessage tNewMsg = new AdapterChannelMessage(tAddress, tPayload);
		
		if(tPipeOut != null){
			tPipeOut.enqueue(tNewMsg);
		}
		else{
			String tMessage = "Gateway: impossibile trovare pipe out per messaggio normale";
			loggerNotify(Type.Error, tMessage, Verbosity.Medium);
		}
		
	}
	
	/**
     * Collega un nuovo ChannelManager al gateway
     * 
     * @param parC ChannelManager da aggiungere alla lista
     */
	public void addNewChannel(ChannelManager parC) {
		
		this.pChannelsMng.add(parC);
		
		this.pWaiter.addWaitable(parC.getInputPipe().getWaitable());
	}
	
	/**
	 * Rimuove una route dalla lista
	 * @param parSrc canale sorgente
	 * @param parDst canale destinazione
	 */
	public void removeRoute(int parSrc, int parDst) {
		
		for(int i = 0; i < this.pClientChannel.size(); i++){
			
			ChannelManager tClientChannel = this.pClientChannel.get(i);
			ChannelManager tServerChannel = this.pServerChannel.get(i);
			if((tClientChannel.getChannelId() == parSrc) &&
			   (tServerChannel.getChannelId() == parDst)){
				
				this.pAdapterServerAddress.remove(i);
				this.pClientChannel.remove(i);
				this.pServerChannel.remove(i);
				break;
				
			}
			
		}
		
		
	}
	
	/**
	 * Aggiunge una route alla lista
	 * @param parSrc canale sorgente
	 * @param parDst canale destinazione
	 */
	public void addRoute(int parSrc, int parDst, FrameAddress parDstAddress){
		
		for(int i = 0; i < this.pChannelsMng.size(); i++ ) {
			
			ChannelManager tTempChannel = this.pChannelsMng.get(i);
			int tCountAdded = 0;
			if ( tTempChannel.getChannelId() == parDst ) {
				this.pServerChannel.add(tTempChannel);
				this.pAdapterServerAddress.add(parDstAddress);
				tCountAdded++;
				if ( tCountAdded == 2 ) {
					break;
				}
			} else if ( tTempChannel.getChannelId() == parSrc){
				this.pClientChannel.add(tTempChannel);
				tCountAdded++;
				if ( tCountAdded == 2 ) {
					break;
				}
			}
		}
		
	}
	
	/**
	 * Trova la pipe corrispondente nelle tabelle statiche di route
	 * @param parPipe pipe in ingresso
	 * @return pipe sulla quale inoltrare il messaggio
	 */
	private Pipe getPipeOutFromPipeIn(Pipe parPipe){
		
		for(int i = 0; i < this.pClientChannel.size(); i++){
			
			ChannelManager tClient = this.pClientChannel.get(i);
			ChannelManager tServer = this.pServerChannel.get(i);
			if(tClient.getInputPipe().equals(parPipe)){
				
				return tServer.getOutputPipe();
				
			}
			else if(tServer.getInputPipe().equals(parPipe)){
				
				return tClient.getOutputPipe();
				
			}
			
		}
		
		return null;
	}
	
	/**
	 * Trova l'indice nell'arraylist delle pipe in entrata
	 * @param parPipe, pipe da confrontare
	 * @return indice cercato
	 */
	public int getIndexFromPipeIn(Pipe<Message> parPipe){

		for(int i = 0; i < this.pClientChannel.size(); i++){
			
			ChannelManager tClient = this.pClientChannel.get(i);
			ChannelManager tServer = this.pServerChannel.get(i);
			if(tClient.getInputPipe().equals(parPipe)){
				
				return i;
				
			}
			else if(tServer.getInputPipe().equals(parPipe)){
				
				return i;
				
			}
			
		}
		
		return -1;
		
	}
	

}